using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using com.bodurov.SilverlightControls.XmlCodeEditor.Enums;
using com.bodurov.SilverlightControls.XmlCodeEditor.Extensions;
using com.bodurov.SilverlightControls.XmlCodeEditor.Utils;

namespace com.bodurov.SilverlightControls.XmlCodeEditor
{
    internal class RichTextBoxContentManager
    {
        private const int MaxUndoStackLength = 5;
        private const int PlatformKeyCode_Back = 8;//Key.Delete
        private const int PlatformKeyCode_Delete = 46;//Key.Back

        private readonly Dictionary<Run, Token> _cache = new Dictionary<Run, Token>(10000);
        private readonly ITokenizer _tokenizer = new SplitTokenizer();
        private readonly XmlCodeEditorBox _box;

        private readonly BackgroundWorker _setContentWorker;

        private readonly LinkedList<IEnumerable<ListOfTokens>> _undoStack = new LinkedList<IEnumerable<ListOfTokens>>();
        private LinkedListNode<IEnumerable<ListOfTokens>> _currentUndoPointer;

        public Dictionary<Run, Token> Cache { get { return this._cache; } }

        private Action<RichTextBoxContentManager> _layoutUpdatedPendingAction;

        public RichTextBoxContentManager(XmlCodeEditorBox box)
        {
            this._box = box;

            this._setContentWorker = new BackgroundWorker();
            this._setContentWorker.DoWork += SetContentWorker_DoWork;
            this._setContentWorker.RunWorkerCompleted += SetContentWorker_RunWorkerCompleted;
        }

        public T GetFromCache<T>(Run run, Converter<Token, T> func)
        {
            return GetFromCache(run, default(T), func);
        }

        public T GetFromCache<T>(Run run, T defaultValue, Converter<Token, T> func)
        {
            if (run == null) return defaultValue;
            Token token;
            if (this._cache.TryGetValue(run, out token))
            {
                return func(token);
            }
            return defaultValue;
        }

        public Token GetTokenByRun(Run run)
        {
            if (run == null) return null;
            return this._cache.ContainsKey(run) ? this._cache[run] : null;
        }

        public bool HasToken(Run run)
        {
            return this._cache.ContainsKey(run);
        }

        public void ClearCache()
        {
            this._cache.Clear();
        }

        public void AddToCache(Run run, Token token)
        {
            this._cache.Add(run, token);
        }

        public bool RemoveFromCache(Run run)
        {
            return this._cache.Remove(run);
        }

        public void ClearParagraph(Paragraph paragraph)
        {
            if (paragraph == null) return;
            var runs = paragraph.GetRuns().ToArray();
            foreach (var run in runs) this._cache.Remove(run);
            paragraph.Inlines.Clear();
        }

        public void RegenerateParagraph(int platformKeyCode, Paragraph paragraph)
        {
            RegenerateParagraph(platformKeyCode, paragraph, null);
        }

        public void RegenerateParagraph(int platformKeyCode, Paragraph paragraph, string whiteSpacePrefix)
        {
            if (paragraph == null) return;
            var isDeleteKey = platformKeyCode == PlatformKeyCode_Back || platformKeyCode == PlatformKeyCode_Delete;
            TextPointer cursorPosition = this._box.Selection.End;
            var restoreCursor = cursorPosition.OverlapsWith(paragraph);
            var cursorOffset = 0;
            if (restoreCursor) cursorOffset = Math.Abs(isDeleteKey
                                                        ? paragraph.ContentStart.GetCharacterOffsetFrom(cursorPosition)
                                                        : cursorPosition.GetCharacterOffsetFrom(paragraph.ContentEnd));

            var content = paragraph.GetContent();

            this.RecreateInlinesForParagraph(paragraph, whiteSpacePrefix + content);

            if (restoreCursor)
            {
                if (String.IsNullOrEmpty(whiteSpacePrefix))
                {
                    var newCursor = isDeleteKey
                                        ? paragraph.ContentStart.GetPointerAtCharacterOffset(cursorOffset)
                                        : paragraph.ContentEnd.GetPointerAtCharacterOffset(cursorOffset * -1);
                    this._box.Selection.Select(newCursor, newCursor);
                }
                else
                {
                    var newCursor = paragraph.ContentStart.GetPointerAtCharacterOffset(whiteSpacePrefix.Length);
                    this._box.Selection.Select(newCursor, newCursor);
                }
            }
        }

        private void RecreateInlinesForParagraph(Paragraph paragraph, string content)
        {
            var lastParagraphRun = paragraph.GetPreviousParagraphLastRun();

            var startingArea = GetFromCache(lastParagraphRun, XmlArea.OutsideTag, t => t.NextArea);

            startingArea &= ~XmlArea.InsideSingleQuoteString;
            startingArea &= ~XmlArea.InsideDualQuoteString;

            var list = _tokenizer.ParseTokens(content, startingArea);
            if (list.Count > 1) throw new Exception("Expected single line");

            this.ClearParagraph(paragraph);

            if (list.Count > 0)
            {
                foreach (var current in list[0])
                {
                    var run = current.ToRun();
                    this.AddToCache(run, current);
                    paragraph.Inlines.Add(run);
                }
            }
        }

        public void RegenerateAll(string value, int cursorOffset)
        {
            if (this._setContentWorker.IsBusy) return;
            this._box.Cursor = Cursors.Wait;
            this._box.IsReadOnly = true;

            this._setContentWorker.RunWorkerAsync(new SetContentWorkerState(value, cursorOffset));
        }

        private void SetContentWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            var state = e.Argument as SetContentWorkerState;

            state.List = this._tokenizer.ParseTokens(state.Text);

            e.Result = state;
        }

        private void SetContentWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            var state = e.Result as SetContentWorkerState;

            if (state != null)
            {
                this.RegenerateParagraphsFromTokens(state.List);

                this._undoStack.AddLast(state.List);
                this._currentUndoPointer = this._undoStack.Last;
                if (this._undoStack.Count > MaxUndoStackLength)
                {
                    this._undoStack.RemoveFirst();
                }

                if (state.CursorOffset > 0 && state.CursorOffset < state.Text.Length)
                {
                    var pointer = this._box.ContentStart.GetPointerAtCharacterOffset(state.CursorOffset);

                    this._box.Selection.Select(pointer, pointer);
                }
                else if (state.CursorOffset == -1 && this._box.Blocks.Count > 0)
                {
                    this._box.Selection.Select(this._box.Blocks[0].ContentStart, this._box.Blocks[0].ContentStart);
                }
            }

            this._box.IsReadOnly = false;
            this._box.Cursor = null;
        }

        private void RegenerateParagraphsFromTokens(IEnumerable<ListOfTokens> list)
        {
            this._box.Blocks.Clear();
            this.ClearCache();

            foreach (var lineOfTokens in list)
            {
                var p = new Paragraph();
                this._box.Blocks.Add(p);
                foreach (var current in lineOfTokens)
                {
                    var run = current.ToRun();
                    this.AddToCache(run, current);
                    p.Inlines.Add(run);
                }
            }
        }

        public void Undo()
        {
            this._currentUndoPointer = this._currentUndoPointer == null
                                            ? this._undoStack.Last
                                            : this._currentUndoPointer.Previous;

            this.RegenerateParagraphsAndResetCursor(this._currentUndoPointer);
        }

        public void Redo()
        {
            this._currentUndoPointer = this._currentUndoPointer == null
                                            ? this._undoStack.Last
                                            : this._currentUndoPointer.Next;

            this.RegenerateParagraphsAndResetCursor(this._currentUndoPointer);
        }

        private void RegenerateParagraphsAndResetCursor(LinkedListNode<IEnumerable<ListOfTokens>> currentUndoPointer)
        {
            if (currentUndoPointer != null)
            {
                this.ResetCursorAfterUndoOrRedo();

                this.RegenerateParagraphsFromTokens(currentUndoPointer.Value);
            }
        }

        private void ResetCursorAfterUndoOrRedo()
        {
            var cursor = this._box.Selection.End;
            var offset = this._box.ContentStart.GetCharacterOffsetFrom(cursor);
            this._layoutUpdatedPendingAction = mgr =>
            {
                var pointer = this._box.ContentStart.GetPointerAtCharacterOffset(offset);
                this._box.Selection.Select(pointer, pointer);
            };
        }



        public void HandleTabClick(string tab, bool shiftIsPressed)
        {
            var run = this._box.Selection.End.GetRun();
            var startParagraph = this._box.Selection.Start.GetParagraph();
            var endParagraph = this._box.Selection.End.GetParagraph();



            if (endParagraph != null)
            {
                if (startParagraph != endParagraph)
                {
                    this.HandleMultilineTab(tab, shiftIsPressed, startParagraph, endParagraph);
                    return;
                }
                if (shiftIsPressed) return;
                var line = tab;
                var pre = line;
                if (run != null)
                {
                    pre = endParagraph.ContentStart.GetSubstring(this._box.Selection.End);
                    pre = pre + tab;
                    var post = this._box.Selection.End.GetSubstring(endParagraph.ContentEnd);
                    line = pre + post;
                }

                this.RecreateInlinesForParagraph(endParagraph, line);

                var newCursor = endParagraph.ContentStart.GetPointerAtCharacterOffset(pre.Length);
                this._box.Selection.Select(newCursor, newCursor);
            }

        }

        private void HandleMultilineTab(string tab, bool shiftIsPressed, Paragraph startParagraph, Paragraph endParagraph)
        {
            var startIndex = this._box.Blocks.IndexOf(startParagraph);
            var endIndex = this._box.Blocks.IndexOf(endParagraph);
            if (startIndex >= endIndex) return;

            for (var i = startIndex; i <= endIndex; i++)
            {
                var paragraph = this._box.Blocks[i] as Paragraph;
                if (paragraph == null) continue;
                if (shiftIsPressed)
                {
                    //var lineContent = paragraph.ContentStart.GetSubstring(paragraph.ContentEnd);
                    //lineContent = ShrinkLineIdentation(lineContent, tab);
                    //this.RecreateInlinesForParagraph(paragraph, lineContent);

                    foreach (var run in paragraph.GetRuns())
                    {
                        var shrunk = ShrinkLineIdentation(run.Text, tab);
                        if (String.IsNullOrEmpty(shrunk))
                        {
                            paragraph.Inlines.Remove(run);
                            this._cache.Remove(run);
                            break;
                        }
                        if (shrunk != run.Text)
                        {
                            run.Text = shrunk;
                            this.GetFromCache(run, t =>
                            {
                                t.Content = shrunk;
                                return t;
                            });
                            break;
                        }
                    }
                }
                else
                {
                    var firstRun = paragraph.GetFirstRun();
                    var t = this.GetTokenByRun(firstRun);
                    var token = new Token { Content = tab, Type = XmlElement.WhiteSpace, Within = t == null ? XmlArea.OutsideTag : t.Within };
                    var run = token.ToRun();
                    this._cache.Add(run, token);
                    paragraph.Inlines.Insert(0, run);
                }
            }

            this._layoutUpdatedPendingAction = mgr =>
            {
                startParagraph = this._box.Blocks[startIndex] as Paragraph;
                endParagraph = this._box.Blocks[endIndex] as Paragraph;
                if (startParagraph != null && endParagraph != null)
                    this._box.Selection.Select(startParagraph.ContentStart, endParagraph.ContentEnd);
            };

        }

        private static string ShrinkLineIdentation(string lineContent, string tab)
        {
            if (!String.IsNullOrEmpty(lineContent))
            {
                var i = 0;
                for (; i < lineContent.Length; i++)
                {
                    if (!Char.IsWhiteSpace(lineContent[i]) || i == tab.Length)
                    {
                        break;
                    }
                }
                if (i > 0) lineContent = lineContent.Substring(i);
            }
            return lineContent;
        }

        public void PerformPaste(string copiedText)
        {
            if (this._setContentWorker.IsBusy) return;

            this._box.Cursor = Cursors.Wait;

            copiedText = copiedText.Replace("\r\n", "\n");

            var contentStart = this._box.ContentStart;
            var contentEnd = this._box.ContentEnd;
            var selectionStart = this._box.Selection.Start;
            var selectionEnd = this._box.Selection.End;

            var preSelection = contentStart.GetSubstring(selectionStart);
            //var selection = selectionStart.GetSubstring(selectionEnd);
            var postSelection = selectionEnd.GetSubstring(contentEnd);

            var preCursor = preSelection + copiedText;

            this._box.SetTextAndPositionCursor(preCursor + postSelection, preCursor.Length);

            //Dbg.Trace("|" + preSelection + "|" + copiedText + "|" + postSelection);



        }

        public void OnLayoutUpdated()
        {
            if (this._layoutUpdatedPendingAction != null)
            {
                this._layoutUpdatedPendingAction(this);
                this._layoutUpdatedPendingAction = null;
            }
        }

        private class SetContentWorkerState
        {
            public SetContentWorkerState(string text, int offset)
            {
                Text = text;
                CursorOffset = offset;
            }

            public readonly string Text;
            public readonly int CursorOffset;
            public List<ListOfTokens> List;
        }


    }
}